import os
import shutil
import hashlib
import tkinter as tk
from tkinter import filedialog, ttk, messagebox
from datetime import datetime
import threading

class FileSyncTool:
    def __init__(self, root):
        self.root = root
        self.root.title("文件同步工具")
        self.root.geometry("800x600")
        self.root.minsize(800, 600)
        
        self.source_path = tk.StringVar()
        self.target_path = tk.StringVar()
        self.status_var = tk.StringVar(value="准备就绪")
        self.log_text = None
        self.progress_var = tk.DoubleVar(value=0.0)
        self.progress_bar = None
        self.sync_thread = None
        
        self._create_ui()
    
    def _create_ui(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding=10)
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 路径选择区域
        path_frame = ttk.LabelFrame(main_frame, text="文件夹路径", padding=10)
        path_frame.pack(fill=tk.X, pady=5)
        
        # 源路径
        ttk.Label(path_frame, text="源文件夹:").grid(row=0, column=0, sticky=tk.W, pady=5)
        ttk.Entry(path_frame, textvariable=self.source_path, width=50).grid(row=0, column=1, padx=5, pady=5)
        ttk.Button(path_frame, text="浏览...", command=self._browse_source).grid(row=0, column=2, padx=5, pady=5)
        
        # 目标路径
        ttk.Label(path_frame, text="目标文件夹:").grid(row=1, column=0, sticky=tk.W, pady=5)
        ttk.Entry(path_frame, textvariable=self.target_path, width=50).grid(row=1, column=1, padx=5, pady=5)
        ttk.Button(path_frame, text="浏览...", command=self._browse_target).grid(row=1, column=2, padx=5, pady=5)
        
        # 同步选项
        options_frame = ttk.LabelFrame(main_frame, text="同步选项", padding=10)
        options_frame.pack(fill=tk.X, pady=5)
        
        self.sync_mode = tk.StringVar(value="bidirectional")
        ttk.Radiobutton(options_frame, text="双向同步", variable=self.sync_mode, value="bidirectional").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        ttk.Radiobutton(options_frame, text="源 → 目标 单向同步", variable=self.sync_mode, value="source_to_target").grid(row=0, column=1, padx=5, pady=5, sticky=tk.W)
        ttk.Radiobutton(options_frame, text="目标 → 源 单向同步", variable=self.sync_mode, value="target_to_source").grid(row=0, column=2, padx=5, pady=5, sticky=tk.W)
        
        self.delete_files = tk.BooleanVar(value=False)
        ttk.Checkbutton(options_frame, text="删除目标位置中不存在于源位置的文件", variable=self.delete_files).grid(row=1, column=0, columnspan=3, padx=5, pady=5, sticky=tk.W)
        
        # 按钮区域
        button_frame = ttk.Frame(main_frame)
        button_frame.pack(fill=tk.X, pady=10)
        
        ttk.Button(button_frame, text="开始同步", command=self._start_sync).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="取消", command=self._cancel_sync).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="比较差异", command=self._compare_folders).pack(side=tk.LEFT, padx=5)
        
        # 进度条
        progress_frame = ttk.Frame(main_frame)
        progress_frame.pack(fill=tk.X, pady=5)
        
        ttk.Label(progress_frame, text="进度:").pack(side=tk.LEFT, padx=5)
        self.progress_bar = ttk.Progressbar(progress_frame, variable=self.progress_var, length=100, mode="determinate")
        self.progress_bar.pack(side=tk.LEFT, fill=tk.X, expand=True, padx=5)
        
        # 状态栏
        status_frame = ttk.Frame(main_frame)
        status_frame.pack(fill=tk.X, pady=5)
        ttk.Label(status_frame, textvariable=self.status_var).pack(side=tk.LEFT, padx=5)
        
        # 日志区域
        log_frame = ttk.LabelFrame(main_frame, text="同步日志", padding=10)
        log_frame.pack(fill=tk.BOTH, expand=True, pady=5)
        
        self.log_text = tk.Text(log_frame, height=10, width=80, wrap=tk.WORD)
        scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview)
        self.log_text.configure(yscrollcommand=scrollbar.set)
        
        self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
        scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    
    def _browse_source(self):
        path = filedialog.askdirectory(title="选择源文件夹")
        if path:
            self.source_path.set(path)
    
    def _browse_target(self):
        path = filedialog.askdirectory(title="选择目标文件夹")
        if path:
            self.target_path.set(path)
    
    def _log(self, message):
        timestamp = datetime.now().strftime("%H:%M:%S")
        self.log_text.insert(tk.END, f"[{timestamp}] {message}\n")
        self.log_text.see(tk.END)
    
    def _calculate_file_hash(self, filepath):
        """计算文件的MD5哈希值"""
        hash_md5 = hashlib.md5()
        with open(filepath, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                hash_md5.update(chunk)
        return hash_md5.hexdigest()
    
    def _get_all_files(self, directory):
        """获取目录中所有文件的相对路径列表"""
        all_files = []
        for root, _, files in os.walk(directory):
            for file in files:
                full_path = os.path.join(root, file)
                rel_path = os.path.relpath(full_path, directory)
                all_files.append(rel_path)
        return all_files
    
    def _compare_folders(self):
        source = self.source_path.get().strip()
        target = self.target_path.get().strip()
        
        if not source or not target:
            messagebox.showerror("错误", "请选择源文件夹和目标文件夹")
            return
        
        if not os.path.exists(source) or not os.path.exists(target):
            messagebox.showerror("错误", "源文件夹或目标文件夹不存在")
            return
        
        self.status_var.set("正在比较文件夹差异...")
        self._log("开始比较文件夹差异...")
        
        # 在新线程中执行比较操作
        threading.Thread(target=self._compare_folders_thread, args=(source, target), daemon=True).start()
    
    def _compare_folders_thread(self, source, target):
        try:
            source_files = set(self._get_all_files(source))
            target_files = set(self._get_all_files(target))
            
            # 文件只在源文件夹中存在
            only_in_source = source_files - target_files
            # 文件只在目标文件夹中存在
            only_in_target = target_files - source_files
            # 两边都存在的文件
            common_files = source_files.intersection(target_files)
            
            # 检查内容不同的文件
            different_content = []
            total_files = len(common_files)
            
            for i, file in enumerate(common_files):
                source_file = os.path.join(source, file)
                target_file = os.path.join(target, file)
                
                source_hash = self._calculate_file_hash(source_file)
                target_hash = self._calculate_file_hash(target_file)
                
                if source_hash != target_hash:
                    different_content.append(file)
                
                # 更新进度
                progress = (i + 1) / total_files * 100 if total_files > 0 else 100
                self.progress_var.set(progress)
                self.root.update_idletasks()
            
            # 显示结果
            self._log(f"比较完成: 源文件夹有 {len(source_files)} 个文件，目标文件夹有 {len(target_files)} 个文件")
            self._log(f"只在源文件夹中: {len(only_in_source)} 个文件")
            self._log(f"只在目标文件夹中: {len(only_in_target)} 个文件")
            self._log(f"内容不同的文件: {len(different_content)} 个文件")
            
            self.status_var.set("文件夹比较完成")
            self.progress_var.set(100)
            
            # 显示详细差异对话框
            self._show_diff_details(only_in_source, only_in_target, different_content)
            
        except Exception as e:
            self._log(f"比较出错: {str(e)}")
            self.status_var.set("比较失败")
    
    def _show_diff_details(self, only_in_source, only_in_target, different_content):
        diff_window = tk.Toplevel(self.root)
        diff_window.title("文件夹差异详情")
        diff_window.geometry("600x500")
        
        notebook = ttk.Notebook(diff_window)
        notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        
        # 只在源文件夹中的文件
        source_frame = ttk.Frame(notebook, padding=10)
        notebook.add(source_frame, text=f"只在源文件夹中 ({len(only_in_source)})")
        
        source_text = tk.Text(source_frame, wrap=tk.NONE)
        source_scrolly = ttk.Scrollbar(source_frame, command=source_text.yview)
        source_scrollx = ttk.Scrollbar(source_frame, orient=tk.HORIZONTAL, command=source_text.xview)
        source_text.configure(yscrollcommand=source_scrolly.set, xscrollcommand=source_scrollx.set)
        
        source_text.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        source_scrolly.grid(row=0, column=1, sticky=(tk.N, tk.S))
        source_scrollx.grid(row=1, column=0, sticky=(tk.E, tk.W))
        
        source_frame.columnconfigure(0, weight=1)
        source_frame.rowconfigure(0, weight=1)
        
        for file in sorted(only_in_source):
            source_text.insert(tk.END, f"{file}\n")
        
        # 只在目标文件夹中的文件
        target_frame = ttk.Frame(notebook, padding=10)
        notebook.add(target_frame, text=f"只在目标文件夹中 ({len(only_in_target)})")
        
        target_text = tk.Text(target_frame, wrap=tk.NONE)
        target_scrolly = ttk.Scrollbar(target_frame, command=target_text.yview)
        target_scrollx = ttk.Scrollbar(target_frame, orient=tk.HORIZONTAL, command=target_text.xview)
        target_text.configure(yscrollcommand=target_scrolly.set, xscrollcommand=target_scrollx.set)
        
        target_text.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        target_scrolly.grid(row=0, column=1, sticky=(tk.N, tk.S))
        target_scrollx.grid(row=1, column=0, sticky=(tk.E, tk.W))
        
        target_frame.columnconfigure(0, weight=1)
        target_frame.rowconfigure(0, weight=1)
        
        for file in sorted(only_in_target):
            target_text.insert(tk.END, f"{file}\n")
        
        # 内容不同的文件
        diff_frame = ttk.Frame(notebook, padding=10)
        notebook.add(diff_frame, text=f"内容不同 ({len(different_content)})")
        
        diff_text = tk.Text(diff_frame, wrap=tk.NONE)
        diff_scrolly = ttk.Scrollbar(diff_frame, command=diff_text.yview)
        diff_scrollx = ttk.Scrollbar(diff_frame, orient=tk.HORIZONTAL, command=diff_text.xview)
        diff_text.configure(yscrollcommand=diff_scrolly.set, xscrollcommand=diff_scrollx.set)
        
        diff_text.grid(row=0, column=0, sticky=(tk.N, tk.S, tk.E, tk.W))
        diff_scrolly.grid(row=0, column=1, sticky=(tk.N, tk.S))
        diff_scrollx.grid(row=1, column=0, sticky=(tk.E, tk.W))
        
        diff_frame.columnconfigure(0, weight=1)
        diff_frame.rowconfigure(0, weight=1)
        
        for file in sorted(different_content):
            diff_text.insert(tk.END, f"{file}\n")
    
    def _start_sync(self):
        source = self.source_path.get().strip()
        target = self.target_path.get().strip()
        
        if not source or not target:
            messagebox.showerror("错误", "请选择源文件夹和目标文件夹")
            return
        
        if not os.path.exists(source) or not os.path.exists(target):
            messagebox.showerror("错误", "源文件夹或目标文件夹不存在")
            return
        
        mode = self.sync_mode.get()
        delete = self.delete_files.get()
        
        # 防止重复启动
        if self.sync_thread and self.sync_thread.is_alive():
            messagebox.showinfo("提示", "同步正在进行中")
            return
        
        self.progress_var.set(0)
        self.status_var.set("正在同步...")
        self._log(f"开始同步 - 模式: {mode}, 删除不存在文件: {delete}")
        
        # 在新线程中执行同步操作
        self.sync_thread = threading.Thread(target=self._sync_folders, args=(source, target, mode, delete), daemon=True)
        self.sync_thread.start()
    
    def _cancel_sync(self):
        if self.sync_thread and self.sync_thread.is_alive():
            # 这里只能设置一个标志，在线程内部检查这个标志来决定是否继续
            self.cancel_sync = True
            self.status_var.set("正在取消同步...")
            self._log("正在取消同步操作...")
        else:
            self.status_var.set("没有正在进行的同步操作")
    
    def _sync_folders(self, source, target, mode, delete):
        try:
            self.cancel_sync = False
            
            if mode == "bidirectional":
                self._sync_bidirectional(source, target, delete)
            elif mode == "source_to_target":
                self._sync_one_way(source, target, delete)
            elif mode == "target_to_source":
                self._sync_one_way(target, source, delete)
            
            if not self.cancel_sync:
                self.status_var.set("同步完成")
                self._log("同步操作已完成")
            else:
                self.status_var.set("同步已取消")
                self._log("同步操作已取消")
                
            self.progress_var.set(100)
            
        except Exception as e:
            self._log(f"同步出错: {str(e)}")
            self.status_var.set("同步失败")
    
    def _sync_bidirectional(self, source, target, delete):
        source_files = self._get_all_files(source)
        target_files = self._get_all_files(target)
        
        source_set = set(source_files)
        target_set = set(target_files)
        
        # 从源到目标的同步
        self._log("执行源到目标的同步...")
        self._sync_files(source, target, source_set, target_set, delete, 0, 50)
        
        if self.cancel_sync:
            return
        
        # 从目标到源的同步
        self._log("执行目标到源的同步...")
        self._sync_files(target, source, target_set, source_set, delete, 50, 100)
    
    def _sync_one_way(self, source, target, delete):
        source_files = self._get_all_files(source)
        target_files = self._get_all_files(target)
        
        source_set = set(source_files)
        target_set = set(target_files)
        
        self._sync_files(source, target, source_set, target_set, delete, 0, 100)
    
    def _sync_files(self, source, target, source_files, target_files, delete, progress_start, progress_end):
        # 需要复制的文件: 源中有但目标中没有的文件，或者两边都有但内容不同的文件
        to_copy = list(source_files - target_files)
        common_files = source_files.intersection(target_files)
        
        # 检查共有文件中内容不同的文件
        for i, file in enumerate(common_files):
            if self.cancel_sync:
                return
                
            source_file = os.path.join(source, file)
            target_file = os.path.join(target, file)
            
            # 检查是否需要更新
            source_mtime = os.path.getmtime(source_file)
            target_mtime = os.path.getmtime(target_file)
            
            # 如果源文件比目标文件新，或者内容不同
            if source_mtime > target_mtime:
                source_hash = self._calculate_file_hash(source_file)
                target_hash = self._calculate_file_hash(target_file)
                
                if source_hash != target_hash:
                    to_copy.append(file)
            
            # 更新进度
            progress = progress_start + (i + 1) / len(common_files) * (progress_end - progress_start) * 0.5
            self.progress_var.set(progress)
            self.root.update_idletasks()
        
        # 复制文件
        for i, file in enumerate(to_copy):
            if self.cancel_sync:
                return
                
            source_file = os.path.join(source, file)
            target_file = os.path.join(target, file)
            
            # 确保目标目录存在
            os.makedirs(os.path.dirname(target_file), exist_ok=True)
            
            try:
                self._log(f"复制: {file}")
                shutil.copy2(source_file, target_file)
            except Exception as e:
                self._log(f"复制失败 {file}: {str(e)}")
            
            # 更新进度
            progress = progress_start + 50 + (i + 1) / len(to_copy) * (progress_end - progress_start) * 0.5
            self.progress_var.set(progress)
            self.root.update_idletasks()
        
        # 删除目标中存在但源中不存在的文件
        if delete:
            to_delete = target_files - source_files
            
            for file in to_delete:
                if self.cancel_sync:
                    return
                    
                target_file = os.path.join(target, file)
                
                try:
                    self._log(f"删除: {file}")
                    os.remove(target_file)
                    
                    # 如果目录为空，也删除目录
                    dir_path = os.path.dirname(target_file)
                    if dir_path and not os.listdir(dir_path):
                        os.rmdir(dir_path)
                except Exception as e:
                    self._log(f"删除失败 {file}: {str(e)}")


if __name__ == "__main__":
    root = tk.Tk()
    app = FileSyncTool(root)
    root.mainloop() 